/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package core;

import org.apache.log4j.Logger;
import org.apache.commons.lang.StringEscapeUtils;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;


//Used for manipulating the xml object
import org.w3c.dom.Document;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;

//Used for creating the xml object
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

//Used for transforming the xml object and writing it to a file
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.File;
import java.math.BigDecimal;
import javax.xml.transform.OutputKeys;
import ui.DbVersioning;


/**
 *
 * @author Sakis Vtdk
 */
public class XmlGenerator
{
	/**
	 * Private Constructor, needed to make use of the Singleton Pattern
	 * 
	 */
	private XmlGenerator()
	{

	}
	
	/**
	 * Method used to get the instance of the XmlGenerator, used to implement
	 * the Singleton Pattern
	 * 
	 * @return XmlGenerator object
	 */
	public static XmlGenerator getXmlGenerator()
	{
		//if the instance does not exist we create it
		if(XmlGenerator._instance == null)
		{
			XmlGenerator._instance = new XmlGenerator();
		}

		return XmlGenerator._instance;
	}


	/**
	 * Method contains the logic of the process of building the XML file
	 * 
	 * @param path The path in which the xml file will be saved. Warning: The path must NOT
	 * contain the <b>[filename]</b>.xml part. The filename of the xml is being generated automatically
	 */
	public void buildXml(String path)
	{
		//create document
		log.info("Creating xml document...");
		this.createXmlDoc();

		//create the attributes of the version element
		Map<String, String> attributes = new HashMap<String, String>();

		//find the versions
		double targetVersion = Database.getDatabase().getCurrentVersion().doubleValue();
		attributes.put(TARGET_VERSION_ATTR, Double.toString(targetVersion));

		//create version element
		Element version = this.createNewElement(VERSION_ELEMENT, "", attributes);

		//add changes elements
		ArrayList<Element> changes_elements = readChangesLogger(BigDecimal.valueOf(targetVersion));
		for(Element element : changes_elements)
		{
			version.appendChild(element);
		}

		//append the new version in the rootElement
		Element rootElement = this.getRootElement();
		rootElement.appendChild(version);

		log.info("Creating Xml file...");
		this.createXmlFile(path + this._database.getDatabaseName() +".xml", true);

		Database.getDatabase().setHasUnsavedChanges(Boolean.FALSE);
	}

	/**
	 * Creates an XML file from the class' xmlDoc based on the arguments passed
	 * 
	 * @param filename The name of the file to be created
	 * @param indentation Whether or not the created file should be indented
	 */
	private void createXmlFile(String filename, boolean indentation)
	{
		try
		{
			//Transformer is used to "transform" an xml tree from various sources
			//so that it can be used with various ways
			TransformerFactory transformerFactory = TransformerFactory.newInstance();
			Transformer transformer = transformerFactory.newTransformer();

			//The source which will be used by the transformer
			DOMSource source = new DOMSource(this._xmlDoc);

			//The stream result which will be used by the transformer
			StreamResult result = new StreamResult(new File(filename));
			
			//check the indentation
			if(indentation)
			{
				transformer.setOutputProperty(OutputKeys.INDENT, "yes");
				transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
			}
			else
			{
				transformer.setOutputProperty(OutputKeys.INDENT, "no");
			}

			//the actual transformation
			transformer.transform(source, result);
		}
		catch(Exception e)
		{
			log.fatal("The xml could not be created!");
		}
	}

	private Element getRootElement()
	{
		Element rootElement = null;
		String path = DbVersioning.DEFAULT_CHANGES_FOLDER + File.separator + this._database.getDatabaseName();

		//if a file with that name already exists, we get the root node
		if(this.fileExists(path +".xml"))
		{
			XmlParser xmlParser = XmlParser.getXmlParser();
			xmlParser.loadXmlFile(path +".xml");
			rootElement = (Element) xmlParser.getRootNode().cloneNode(true); 
			rootElement = (Element) this._xmlDoc.importNode(rootElement,true);
			this._xmlDoc.appendChild(rootElement);
		}
		else
		{
			//create root element
			log.info("Creating root element...");
			rootElement  = this.createNewElement(this._database.getDatabaseName(), "", null);
			this._xmlDoc.appendChild(rootElement);
		}

		return rootElement;
	}


	/**
	 * Parses all the changes and creates the corresponding DOM elements
	 * 
	 * @return an <code>ArrayList<element></code> is returned containing all the elements created by the method
	 */
	private ArrayList<Element> readChangesLogger(BigDecimal targetVersion)
	{
		ChangesLogger changesLogger = ChangesLogger.getChangesLogger();
		ArrayList<Change> changes = changesLogger.getChanges();
		ArrayList<Element> elements = new ArrayList();

		for(Change ch : changes)
		{
			//make sure only the latest changes will be added
			//that is all the changes that have base version equal (or greater) 
			//to the current base version of the database
			if(ch.getTargetVersion().compareTo(targetVersion) >= 0)
			{

				//first create the elements that are common for all kinds of changes
				// i.e. Create | Update | Delete 

				//create the <change type="something"> ... </change> element first
				Map<String, String> attributes = new HashMap<String, String>();
				attributes.put(CHANGE_TYPE_ATTR, ch.getType());
				Element change = this.createNewElement(CHANGE_ELEMENT, "", attributes);
				
				//create and append the <object_changed>[column|table]</object_changed>
				Element element = this.createNewElement(OBJECT_CHANGED_ELEMENT, ch.getObjectChanged(), null);
				change.appendChild(element);
				
				//create and append the <parent_id>[parent_id]</parent_id> element
				String parent_id_value;
				if(ch.getObjectChanged().equals(Change.ObjectChanged.TABLE.toString()))
				{
					parent_id_value = "-1";
				}
				else
				{
					parent_id_value = Integer.toString(ch.getParentId());
				}
				Element parent_id = this.createNewElement(PARENT_ID_ELEMENT, parent_id_value, null);
				change.appendChild(parent_id);

				//if we talk about a column, we need the parent name
				if(ch.getObjectChanged().equals(Change.ObjectChanged.COLUMN.toString()))
				{
					String parent_name_value = ch.getParentName();
					Element parent_name = this.createNewElement(PARENT_NAME_ELEMENT, parent_name_value, null);
					change.appendChild(parent_name);
				}

				//create and append the id
				String id_value = Integer.toString(ch.getId());
					
				Element id = this.createNewElement(ID_ELEMENT, id_value, null);
				change.appendChild(id);

				//create and append the <name>[name]</name> element
				Element name = this.createNewElement(NAME_ELEMENT, ch.getName(), null);
				change.appendChild(name);

				//and finally add the elements required if the change is "UPDATE"
				if(ch.getType().equalsIgnoreCase(Change.Type.UPDATE.toString()))
				{
					//first note what has changed
					Element what_changed = this.createNewElement(WHAT_CHANGED_ELEMENT, ch.getWhatChanged(), null);
					change.appendChild(what_changed);

					//then note the old and the new value
					Element previous_value = this.createNewElement(OLD_VALUE_ELEMENT, ch.getOldValue(), null);
					change.appendChild(previous_value);

					Element new_value = this.createNewElement(NEW_VALUE_ELEMENT, ch.getNewValue(), null);
					change.appendChild(new_value);
				}

				//for all kinds of changes we add the summary of the change, which is 
				//only informational
				Element summary = this.createNewElement(SUMMARY_ELEMENT, ch.getChangeSummary(), null);
				change.appendChild(summary);
					
				elements.add(change);
			}
		
		}
		
		return elements;

	}

	
	/**
	 * Creates an empty XmlDocument
	 */
	private void createXmlDoc()
	{
		try
		{
			DocumentBuilderFactory documentFactory = DocumentBuilderFactory.newInstance();
			DocumentBuilder documentBuilder = documentFactory.newDocumentBuilder();
			this._xmlDoc = documentBuilder.newDocument();	
		}
		catch(ParserConfigurationException e)
		{
			log.fatal("XML document creation failed: " + e);
		}
	}


	/**
	 * Creates an element based on the values given
	 * 
	 * @param name The name of the element (tag)
	 * @param value The value of the element <name>value</name>
	 * @param attributes Possible attributes the element may have: <name attrib1="something">value</name>
	 * @return The element created
	 */
	private Element createNewElement(String name, String value, Map<String, String> attributes)
	{
		log.info("Creating element '"+ name +"'...");
		Element element = this._xmlDoc.createElement(name);

		//if there is a value
		if(value != null && !value.isEmpty())
		{
			value = StringEscapeUtils.escapeXml(value);
			log.debug("Element value: '"+ value +"'");
			element.appendChild(this._xmlDoc.createTextNode(value));
		}

		//if there are attributes
		if(attributes != null && !attributes.isEmpty())
		{
			log.debug("Inserting attributes...");
			element = this.insertAttributes(element, attributes);
		}
		
		return element;
	}

	/**
	 * Method used to insert attributes to an XML element
	 * 
	 * @param element The DOM Element on which the attributes will be inserted
	 * @param attributes A HashMap containing the attributes to be inserted
	 * @return The DOM Element with the attributes
	 */
	private Element insertAttributes(Element element, Map<String, String> attributes)
	{
		for(Map.Entry<String, String> entry : attributes.entrySet())
		{
			element.setAttribute(entry.getKey(), entry.getValue());
			log.debug("Attribute: '"+ entry.getKey() +"' = '"+ entry.getValue() +"'");
		}

		return element;
	}

	/**
	 * Checks whether a file with this name already exists
	 * @param filename
	 * @return True if the file exists, false otherwise
	 */
	private boolean fileExists(String filename)
	{
		File tmpFile = new File(filename);

		return (tmpFile.exists() && tmpFile.isFile()) ? true : false;
	}

	private static XmlGenerator _instance = null;
	private Document _xmlDoc;
	private Database _database = Database.getDatabase();

	//the names of the nodes and the attributes
	public static final String VERSION_ELEMENT = "version";
	public static final String TARGET_VERSION_ATTR = "target";
	public static final String CHANGE_ELEMENT = "change";
	public static final String CHANGE_TYPE_ATTR = "type";
	public static final String OBJECT_CHANGED_ELEMENT = "object_changed";
	public static final String PARENT_ID_ELEMENT = "parent_id";
	public static final String ID_ELEMENT = "id";
	public static final String WHAT_CHANGED_ELEMENT = "what_changed";
	public static final String OLD_VALUE_ELEMENT = "old_value";
	public static final String NEW_VALUE_ELEMENT = "new_value";
	public static final String SUMMARY_ELEMENT = "summary";
	public static final String NAME_ELEMENT = "name";
	public static final String PARENT_NAME_ELEMENT = "parent_name";


	//logger instance
	private static Logger log = Logger.getLogger(XmlGenerator.class);
}
